package cn.juque.common.wx;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.juque.common.constants.MessageEnum;
import cn.juque.common.constants.WxConstants;
import cn.juque.common.exception.AppException;
import cn.juque.common.fastjson.abstracts.AbstractTypeReference;
import cn.juque.common.utils.FastJsonUtil;
import cn.juque.common.wx.dto.wxpay.CreatePrepayOrderRequestDTO;
import cn.juque.common.wx.dto.wxpay.PayNotifyDTO;
import cn.juque.common.wx.dto.wxpay.PayNotifyOrderInfoDTO;
import cn.juque.common.wx.dto.wxpay.PayNotifyResourceDTO;
import com.alibaba.fastjson2.JSONObject;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.util.Map;
import java.util.Set;

/**
 * @author nuoka
 * @version 1.0.0
 * <li>IntelliJ IDEA</li>
 * <li></li>
 * @date 2021/10/2 10:18
 **/
@Slf4j
public class WxPayHelper {

    /**
     * 私钥
     */
    @Value("${wx.pay.private.key}")
    private String privateKey;

    /**
     * 商户号
     */
    @Value("${wx.pay.mch-id}")
    private String mchId;

    /**
     * 商户证书序列号
     */
    @Value("${wx.pay.mch-serial-no}")
    private String mchSerialNo;

    /**
     * V3密钥
     */
    @Value("${wx.pay.api-v3-key}")
    private String apiV3Key;

    private AutoUpdateCertificatesVerifier verifier;

    private AesUtil aesUtil;

    private CloseableHttpClient payHttpClient;

    /**
     * 初始化httpClient
     */
    public void initHttpClient() {
        // 加载商户私钥（privateKey：私钥字符串）
        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(
            StandardCharsets.UTF_8)));
        // 加载平台证书（mchId：商户号,mchSerialNo：商户证书序列号,apiV3Key：V3密钥）
        this.verifier = new AutoUpdateCertificatesVerifier(
            new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes(StandardCharsets.UTF_8));
        this.aesUtil =new AesUtil(apiV3Key.getBytes(StandardCharsets.UTF_8));
        // 初始化httpClient
        this.payHttpClient = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, merchantPrivateKey).withValidator(new WechatPay2Validator(verifier)).build();
    }

    /**
     * 创建预支付订单
     * @param requestDTO 入参
     * @return prepay_id
     */
    public String createOrder(CreatePrepayOrderRequestDTO requestDTO) {
        requestDTO.setMchId(CharSequenceUtil.isEmpty(requestDTO.getMchId()) ? this.mchId : requestDTO.getMchId());
        // 请求body参数
        String reqBody = FastJsonUtil.toJsonString(requestDTO);
        StringEntity entity = new StringEntity(reqBody,StandardCharsets.UTF_8);
        entity.setContentType(ContentType.APPLICATION_JSON.toString());
        //请求URL
        HttpPost httpPost = new HttpPost(WxConstants.WX_PAY_CREATE_ORDER);
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        //完成签名并执行请求
        try (CloseableHttpResponse response = payHttpClient.execute(httpPost)) {
            int statusCode = response.getStatusLine().getStatusCode();
            Set<Integer> successCodeSet = CollUtil.newHashSet(HttpStatus.SC_OK, HttpStatus.SC_NO_CONTENT);
            String body = EntityUtils.toString(response.getEntity());
            if(successCodeSet.contains(statusCode)) {
             log.info("create prepay id successful, body:{}", body);
                JSONObject jsonObject = FastJsonUtil.parseObject(body);
                return jsonObject.getString("prepay_id");
            } else {
                log.error("create prepay id fail, status code:{}, body:{}", statusCode, body);
                throw new AppException(MessageEnum.CREATE_WX_PREPAY_ID_ERROR);
            }
        } catch (Exception e) {
            httpPost.abort();
            log.error("create prepay id fail, body:{}", reqBody, e);
            throw new AppException(MessageEnum.CREATE_WX_PREPAY_ID_ERROR);
        }
    }

    /**
     * 查询订单
     * @param outTradeNo 订单号
     * @return PayNotifyOrderInfoDTO
     */
    public PayNotifyOrderInfoDTO orderDetail(String outTradeNo) {
        //请求URL
        String url = CharSequenceUtil.format(WxConstants.QUERY_ORDER_DETAIL, outTradeNo, this.mchId);
        HttpGet httpGet = new HttpGet(url);
        try (CloseableHttpResponse response = payHttpClient.execute(httpGet)) {
            int statusCode = response.getStatusLine().getStatusCode();
            Set<Integer> successCodeSet = CollUtil.newHashSet(HttpStatus.SC_NO_CONTENT);
            String body = EntityUtils.toString(response.getEntity());
            if(successCodeSet.contains(statusCode)) {
                log.info("query order successful, outTradeNo:{}", outTradeNo);
                return FastJsonUtil.parseObject(body, new AbstractTypeReference<PayNotifyOrderInfoDTO>() {});
            } else {
                log.error("close order fail, status code:{}, body:{}", statusCode, body);
                throw new AppException(MessageEnum.WX_QUERY_ORDER_ERROR);
            }
        } catch (Exception e) {
            httpGet.abort();
            log.error("close order fail, url:{}", url, e);
            throw new AppException(MessageEnum.WX_QUERY_ORDER_ERROR);
        }
    }

    /**
     * 关闭订单
     * @param outTradeNo 订单号
     */
    public void closeOrder(String outTradeNo) {
        Map<String, String> param = MapUtil.newHashMap(1);
        param.put("mchid", this.mchId);
        // 请求body参数
        String reqBody = FastJsonUtil.toJsonString(param);
        StringEntity entity = new StringEntity(reqBody,StandardCharsets.UTF_8);
        entity.setContentType(ContentType.APPLICATION_JSON.toString());
        //请求URL
        String url = CharSequenceUtil.format(WxConstants.CLOSE_ORDER, outTradeNo);
        HttpPost httpPost = new HttpPost(url);
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        try (CloseableHttpResponse response = payHttpClient.execute(httpPost)) {
            int statusCode = response.getStatusLine().getStatusCode();
            Set<Integer> successCodeSet = CollUtil.newHashSet(HttpStatus.SC_NO_CONTENT);
            String body = EntityUtils.toString(response.getEntity());
            if(successCodeSet.contains(statusCode)) {
                log.info("close order successful, outTradeNo:{}", outTradeNo);
            } else {
                log.error("close order fail, status code:{}, body:{}", statusCode, body);
                throw new AppException(MessageEnum.WX_CLOSE_ORDER_ERROR);
            }
        } catch (Exception e) {
            httpPost.abort();
            log.error("close order fail, body:{}", reqBody, e);
            throw new AppException(MessageEnum.WX_CLOSE_ORDER_ERROR);
        }
    }

    /**
     * 回调信息解密
     * @param headers 请求头信息
     * @param body 报文
     * @return PayNotifyResourceDTO
     * @throws GeneralSecurityException GeneralSecurityException
     * @throws IOException IOException
     */
    public PayNotifyOrderInfoDTO decodePayNotify(Map<String,Object> headers, String body) throws GeneralSecurityException, IOException {
        PayNotifyDTO payNotifyDTO = FastJsonUtil.parseObject(body, new AbstractTypeReference<PayNotifyDTO>() {});
        //校验事件是否为支付通知
        if (!WxConstants.EVENT_TYPE_TRANSACTION_SUCCESS.equals(payNotifyDTO.getEventType())
            || !WxConstants.RESOURCE_TYPE_ENCRYPT_RESOURCE.equals(payNotifyDTO.getResourceType())) {
            throw new AppException(MessageEnum.SYSTEM_ERROR);
        }
        // 构建应答请求签名串
        String timestamp = headers.get("Wechatpay-Timestamp").toString();
        String nonce = headers.get("Wechatpay-Nonce").toString();
        String serial = headers.get("Wechatpay-Serial").toString();
        String signature = headers.get("Wechatpay-Signature").toString();
        // 校验微信支付回调请求签名
        String message = timestamp + "\n" + nonce + "\n" + body + "\n";
        //校验签名
        boolean verifierFlag = this.verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature);
        if(!verifierFlag) {
            throw new AppException(MessageEnum.WX_PAY_VERIFY_ERROR);
        }
        //解密resource
        PayNotifyResourceDTO payNotifyResourceDTO =payNotifyDTO.getResource();
        String resourceText = this.aesUtil.decryptToString(
            payNotifyResourceDTO.getAssociatedData().getBytes(StandardCharsets.UTF_8),
            payNotifyResourceDTO.getNonce().getBytes(StandardCharsets.UTF_8),
            payNotifyResourceDTO.getCipherText());
        return FastJsonUtil.parseObject(resourceText, new AbstractTypeReference<PayNotifyOrderInfoDTO>() {});
    }

    /**
     * 关闭
     * @throws IOException IOException
     */
    public void closeHttpClient() throws IOException {
        this.payHttpClient.close();
    }

}
